Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit a345321047f7e2c33ecb8bab1e11aab37395fbe4


Parents : 5b81f24
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-04-23T13:17:26-05:00

feat(tests): add property-based tests for hex colour parsing and repository upload filename sanitization to enhance robustness

Changes
Diff

diff --git a/tests/backend/test_colour_utils_hypothesis.py b/tests/backend/test_colour_utils_hypothesis.py
new file mode 100644
index 00000000..ef6df407
--- /dev/null
+++ b/tests/backend/test_colour_utils_hypothesis.py
@@ -0,0 +1,46 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Property tests for hex colour parsing."""
+
+import pytest
+from hypothesis import given, settings
+from hypothesis import strategies as st
+
+from meshchatx.src.backend.colour_utils import ColourUtils
+
+
+@settings(max_examples=200, deadline=None)
+@given(t=st.text(max_size=200))
+def test_hex_colour_only_value_error_or_returns_bytes(t):
+ try:
+ out = ColourUtils.hex_colour_to_byte_array(t)
+ except ValueError:
+ return
+ except Exception as e:
+ raise AssertionError(f"unexpected exception: {type(e).__name__}: {e}") from e
+
+ assert isinstance(out, bytes)
+
+
+@settings(max_examples=150, deadline=None)
+@given(
+ h=st.from_regex(r"[0-9a-fA-F]{2,64}", fullmatch=True).filter(
+ lambda x: len(x) % 2 == 0
+ )
+)
+def test_hex_colour_even_length_hex_round_trip(h):
+ out = ColourUtils.hex_colour_to_byte_array(h)
+ out_hash = ColourUtils.hex_colour_to_byte_array("#" + h)
+ assert out == out_hash
+ assert len(out) == len(h) // 2
+
+
+@settings(max_examples=80, deadline=None)
+@given(
+ h=st.from_regex(r"[0-9a-fA-F]{1,63}", fullmatch=True).filter(
+ lambda x: len(x) % 2 == 1
+ )
+)
+def test_hex_colour_odd_length_raises(h):
+ with pytest.raises(ValueError):
+ ColourUtils.hex_colour_to_byte_array(h)

diff --git a/tests/backend/test_repository_server_manager_hypothesis.py b/tests/backend/test_repository_server_manager_hypothesis.py
new file mode 100644
index 00000000..0134e33b
--- /dev/null
+++ b/tests/backend/test_repository_server_manager_hypothesis.py
@@ -0,0 +1,60 @@
+# SPDX-License-Identifier: 0BSD
+
+"""Property tests for repository upload filename sanitization."""
+
+import os
+import re
+import tempfile
+
+from hypothesis import given, settings
+from hypothesis import strategies as st
+
+from meshchatx.src.backend.repository_server_manager import (
+ RepositoryServerManager,
+ _safe_any_upload_filename,
+)
+
+_SAFE_RE = re.compile(r"[A-Za-z0-9._+\-]+")
+
+
+@settings(max_examples=250, deadline=None)
+@given(name=st.text(max_size=400))
+def test_safe_any_upload_filename_never_raises(name):
+ out = _safe_any_upload_filename(name)
+ if out is None:
+ return
+ assert isinstance(out, str)
+ assert out == os.path.basename(name)
+ assert out == name
+ assert ".." not in out
+ assert _SAFE_RE.fullmatch(out)
+
+
+@settings(max_examples=200, deadline=None)
+@given(
+ base=st.from_regex(r"[A-Za-z0-9._+\-]{1,120}", fullmatch=True).filter(
+ lambda b: b not in (".", "..") and ".." not in b
+ ),
+)
+def test_save_upload_roundtrip_for_generated_safe_names(base):
+ with tempfile.TemporaryDirectory() as td:
+ mgr = RepositoryServerManager(td)
+ ok, err = mgr.save_upload(base, b"\x00\x01\x02")
+ assert ok and err is None
+ dest = os.path.join(td, "repository-server", "uploads", base)
+ assert dest == os.path.realpath(dest)
+ assert os.path.isfile(dest)
+ assert os.path.dirname(dest) == os.path.join(td, "repository-server", "uploads")
+
+
+@settings(max_examples=120, deadline=None)
+@given(
+ prefix=st.text(max_size=80),
+ base=st.from_regex(r"[A-Za-z0-9._+\-]{1,40}", fullmatch=True),
+)
+def test_save_upload_rejects_path_traversal_prefix(prefix, base):
+ poison = prefix + "../" + base
+ with tempfile.TemporaryDirectory() as td:
+ mgr = RepositoryServerManager(td)
+ ok, err = mgr.save_upload(poison, b"x")
+ assert not ok

diff --git a/tests/electron/safeExternalUrl.test.js b/tests/electron/safeExternalUrl.test.js
index 0bb1ecdf..93503579 100644
--- a/tests/electron/safeExternalUrl.test.js
+++ b/tests/electron/safeExternalUrl.test.js
@@ -41,4 +41,19 @@ describe("safeExternalUrl", () => {
}
}
});
+
+ it("fuzz: full BMP code units do not throw and only yield safe schemes", () => {
+ for (let i = 0; i < 250; i++) {
+ let s = "";
+ const len = Math.floor(Math.random() * 400);
+ for (let j = 0; j < len; j++) {
+ s += String.fromCharCode(Math.floor(Math.random() * 65536));
+ }
+ expect(() => normalizeExternalUrlForOpen(s)).not.toThrow();
+ const o = normalizeExternalUrlForOpen(s);
+ if (o !== null) {
+ expect(o.startsWith("http://") || o.startsWith("https://") || o.startsWith("mailto:")).toBe(true);
+ }
+ }
+ });
});

diff --git a/tests/electron/shellPathGuard.test.js b/tests/electron/shellPathGuard.test.js
index 04220bc6..97854bc0 100644
--- a/tests/electron/shellPathGuard.test.js
+++ b/tests/electron/shellPathGuard.test.js
@@ -57,4 +57,27 @@ describe("shellPathGuard", () => {
fs.unlinkSync(p);
}
});
+
+ it("fuzz: paths under default storage stay allowed", () => {
+ for (let i = 0; i < 200; i++) {
+ const seg = Array.from({ length: 10 }, () => String.fromCharCode(Math.floor(Math.random() * 94) + 33)).join(
+ ""
+ );
+ const p = path.join(storage, "fuzz-shell", seg);
+ expect(() => isAllowedShellPath(p, ctx)).not.toThrow();
+ expect(isAllowedShellPath(p, ctx)).toBe(true);
+ }
+ });
+
+ it("fuzz: random absolute-looking paths return boolean without throw", () => {
+ for (let i = 0; i < 200; i++) {
+ let s = process.platform === "win32" ? "C:\\" : "/";
+ const n = Math.floor(Math.random() * 24) + 4;
+ for (let j = 0; j < n; j++) {
+ s += String.fromCharCode(Math.floor(Math.random() * 96) + 32);
+ }
+ expect(() => isAllowedShellPath(s, ctx)).not.toThrow();
+ expect(typeof isAllowedShellPath(s, ctx)).toBe("boolean");
+ }
+ });
});

diff --git a/tests/frontend/LinkUtils.test.js b/tests/frontend/LinkUtils.test.js
index 792da3ca..20a68216 100644
--- a/tests/frontend/LinkUtils.test.js
+++ b/tests/frontend/LinkUtils.test.js
@@ -178,4 +178,24 @@ describe("LinkUtils.js", () => {
expect(LinkUtils.httpUrlHrefOrNull("file:///etc/passwd")).toBeNull();
});
});
+
+ describe("fuzzing robustness", () => {
+ it("httpUrlHrefOrNull and renderAllLinks tolerate random BMP strings", () => {
+ for (let i = 0; i < 300; i++) {
+ let s = "";
+ const len = Math.floor(Math.random() * 600);
+ for (let j = 0; j < len; j++) {
+ s += String.fromCharCode(Math.floor(Math.random() * 65536));
+ }
+ expect(() => LinkUtils.httpUrlHrefOrNull(s)).not.toThrow();
+ expect(() => LinkUtils.renderAllLinks(s)).not.toThrow();
+ const h = LinkUtils.httpUrlHrefOrNull(s);
+ if (h !== null) {
+ expect(h.startsWith("http://") || h.startsWith("https://")).toBe(true);
+ }
+ const rendered = LinkUtils.renderAllLinks(s);
+ expect(rendered.toLowerCase()).not.toContain("<script");
+ }
+ });
+ });
});


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────